Skip to content

Conversation

@ishajagadish
Copy link
Collaborator

Resolves #4035

Added an “AI-generated” icon and tooltip across gallery, admin/label map, and validate views. The frontend uses new AiLabelIndicator helpers and CSS tweaks so the icon sits on the marker corner, and a tooltip explains the label was AI-generated. On the backend, LabelTable.getRecentLabelsMetadata flags ai_generated via a SQL EXISTS check on labels placed by users with the AI role, and that flag is exposed through LabelFormats/AdminController to drive the UI.

Before/After screenshots

Before:
Screenshot 2025-12-11 at 2 19 36 PM

After:
Screenshot 2025-12-11 at 2 18 55 PM

Testing instructions
  1. Navigate to any of the following pages: /validate, /expertValidate, /mobileValidate, /gallery, /labelMap, /admin (the "Map" tab), /admin (the "Labels" tab), /admin (the "Label Search" tab), /admin/label.
  2. Notice the new AI icon on any labels that were AI-generated.
  3. Hover over this label to see the tooltip message.
Things to check before submitting the PR
  • I've written a descriptive PR title.
  • I've added/updated comments for large or confusing blocks of code.
  • I've included before/after screenshots above.
  • I've asked for and included translations for any user facing text that was added or modified.
  • I've updated any logging. Clicks, keyboard presses, and other user interactions should be logged. If you're not sure how (or if you need to update the logging), ask Mikey. Then make sure the documentation on this wiki page is up to date for the logs you added/updated.
  • I've tested on mobile (only needed for validation page).

…logic to check SQL tables. Added a tooltip message that pops up on hover across gallery, admin, and validate pages.
Copy link
Member

@misaugstad misaugstad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks incredibly solid, great work!! Some comments:

  1. Looks like when switching out the AI icon, something was missed on the /admin/label/ search page:
    image
  2. There are references to admin-ai-icon-header... Are those remnants of when you had an AI icon next to the label type name in the popup headers? If so, there's some stuff left to clean out there! If not, what does it do?
  3. I didn't have a chance to carefully look through the three AiLabelIndicator.js files, but I assume that they have quite a bit of overlap. Do you think that it makes sense to try to consolidate them? We can talk in our meeting about how they're differentiated.
  4. There are some more comments in the code, including some about writing more efficient queries!

at.stale,
comment.comments
comment.comments,
EXISTS (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that, instead of adding a subquery here, it would be more efficient to just do the joins in the main query. So within the query it would look like

...
INNER JOIN sidewalk_user AS u ON at.user_id = u.user_id
INNER JOIN user_role AS ur ON u.user_id = ur.user_id
INNER JOIN role AS r ON ur.role_id = r.role_id
INNER JOIN label_point AS lp ON lb1.label_id = lp.label_id
...

And then you could just refer to it as r.role = 'AI' rather than doing the subquery in the SELECT statement.

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@misaugstad I removed the per-row EXISTS subquery in getRecentLabelsMetadata and pulled the role check into the main query: we now join user_role/role once and select (ai_user.user_id IS NOT NULL) AS ai_generated, using r.role = 'AI' from the join instead of a subquery.

if (includeAiTags) la.flatMap(_.tags).getOrElse(List.empty[String].bind).asColumnOf[Option[List[String]]]
else None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]]
else None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]],
isAiUser(l.userId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same comment above applies to these Slick queries as well. Rather than having this function that essentially creates a subquery for each row, we can do the joins above:

val _labelInfo = for {
      (_lb, _at, _us) <- labelsWithAuditTasksAndUserStats
      _ur <- userRoles if _us.userId === _ur.userId
      _r <- roleTable if _ur.roleId === _r.roleId
      ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would apply this to all the queries that you have, even if I didn't explicitly comment on them.

aiIcon.setAttribute('data-toggle', 'tooltip');
aiIcon.setAttribute('data-placement', 'top');
aiIcon.setAttribute('title', i18next.t('common:ai-disclaimer', { aiVal: aiValidation }));
aiIcon.setAttribute('title', i18next.t('common:ai-generated-label-tooltip'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't test this change, but I'm assuming it's a mistake?

… fixed /admin/label/search icon issue, cleaned up code.
Copy link
Member

@misaugstad misaugstad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at LabelMap, and this is not what the table was supposed to look like:
image

It used to look like this
image

Various comments throughout the code as well!

at.stale,
comment.comments
comment.comments,
EXISTS (

This comment was marked as resolved.

…ltip z-index, added comments, reordered AiLabelIndicator.js
@misaugstad
Copy link
Member

@ishajagadish I've fixed the issue with the inner joins.

  • For the raw SQL query, the inner join worked as I had expected
  • For the Slick queries, for some reason Slick was generating a cross join when we tried to do the inner join at the place where we were. I was able to get around this by passing along _r.role === 'AI' instead of passing through the whole table. Not totally sure why that prevents Slick from creating a cross join that we don't need, but it worked!

I've fixed this in all four queries (Gallery, Validate, LabelMap, and the label popup).

Copy link
Member

@misaugstad misaugstad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made some very minor tweaks, and we're ready to go live with it! 🎉

@misaugstad misaugstad merged commit 37c1512 into develop Jan 16, 2026
@misaugstad misaugstad deleted the 4035-ai-generated-label-indicator branch January 16, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add AI generated label indicator

3 participants